const _raw = Symbol('raw')
export function isReadonly<T>(obj: T): obj is Readonly<T> {
  return obj && typeof obj === 'object' &&  (obj as unknown as any)[_raw] !== undefined
}
export function unreadonly<T>(obj: T): T {
  if (obj) {
    const raw = (obj as unknown as { [_raw]?: T })[_raw] as T
    if (raw) return raw
  }
  return obj
}
export function readonly<T>(obj: T, visited: Array<{ raw: T; proxy: T }> = []): T {
  if (!obj || typeof obj !== 'object') return obj
  if (obj instanceof Date) {
    return obj
  }
  const visitedInfo = visited.find((v) => v.raw === obj)
  if (visitedInfo) return visitedInfo.proxy
  if ((obj as any)[_raw] !== undefined) return obj

  const baseHandler = {
    get(target: object, property: string | symbol) {
      if (property === _raw) return target
      const value = (target as any)[property]
      if (value instanceof Date) {
        return value
      }
      if (value && typeof value === 'object') {
        if (value[_raw] !== undefined) return value
        return readonly(value, visited)
      }
      return value
    },
    set(target: object, property: string | symbol) {
      throw new Error('cannot set property on readonly object')
    },
    deleteProperty(target: object, property: string | symbol) {
      throw new Error('cannot delete property on readonly object')
    },
    defineProperty(target: object, property: string | symbol) {
      throw new Error('cannot define property on readonly object')
    },
  }

  const arrayHandler = {
    ...baseHandler,
    get(target: object, property: string | symbol) {
      if (property === _raw) return target
      const value = (target as any)[property]
      if (value) {
        if (value instanceof Date) {
          return value
        }
        if (typeof value === 'object') {
          if (value[_raw] !== undefined) return value
          return readonly(value, visited)
        }
        const mutateMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort', 'fill']
        if (typeof value === 'function' && mutateMethods.includes(property as string)) {
          return (...args: unknown[]) => {
            throw new Error('cannot modify readonly array')
          }
        }
        return value
      }
      return value
    },
  }

  const handler = Array.isArray(obj) ? arrayHandler : baseHandler
  const proxy = new Proxy(obj as object, handler) as T
  visited.push({ raw: obj, proxy })
  return proxy
}
